<?php

/**
 * Template Handler
 *
 * @author MeanEYE
 */

class TemplateHandler {
	/**
	 * Used for debugging
	 * @var string
	 */
	public $file;

	/**
	 * XML parser
	 * @var resource
	 */
	public $engine;

	/**
	 * Raw XML data
	 * @var string
	 */
	private $data;

	/**
	 * If XML parser is active and ready
	 * @var boolean
	 */
	public $active;

	/**
	 * Transfer params available from within template
	 * @var array
	 */
	private $params;

	/**
	 * Handling module name
	 * @var object
	 */
	public $module;

	/**
	 * Custom tag handlers
	 * @var array
	 */
	private $handlers = array();
	
	/**
	 * List of tags that shouldn't be closed
	 * @var array
	 */
	private $tags_without_end = array('br', 'hr', 'img', 'base', 'input', 'link');
	
	/**
	 * If we should close all tags
	 * @var boolean
	 */
	private $close_all_tags = false;
	
	/**
	 * List of session variables that we protect from setting
	 * @var array
	 */
	private $protected_variables = array('uid', 'logged', 'level', 'username', 'fullname', 'captcha');

	/**
	 * Constructor
	 *
	 * @param string $file
	 * @return TemplateHandler
	 */
	public function __construct($file = "", $path = "") {
		global $template_path;

		$this->file = $path.$file;
		$this->active = false;
		$this->params = array();
		$this->module = null;
		$path = (empty($path)) ? $template_path : $path;

		// if file exits then load
		if (!empty($file) && file_exists($path.$file)) {
			$this->data = @file_get_contents($path.$file);
			$this->engine = new XMLParser($this->data, $path.$file);
			$this->engine->Parse();

			$this->active = true;
		}
	}


	/**
	 * Restores XML to original state
	 */
	public function restoreXML() {
		if (isset($this->engine))
			$this->engine->Parse();
	}

	/**
	 * Manually set XML
	 * @param string $data
	 */
	public function setXML($data) {
		if (isset($this->engine))
			unset($this->engine);

		$this->engine = new XMLParser($data, '');
		$this->engine->Parse();
		$this->active = true;
	}

	/**
	 * Sets local params
	 *
	 * @param array $params
	 */
	public function setLocalParams($params) {
		$this->params = $params;
	}

	/**
	 * Sets mapped module name for section content parsing
	 *
	 * @param string $module
	 */
	public function setMappedModule($module) {
		if (is_string($module)) {
			if (class_exists($module))
				$this->module = call_user_func(array($module, 'getInstance'));
		} else {
			$this->module = $module;
		}
	}

	/**
	 * Parse loaded template
	 *
	 * @param integer $level Current level of parsing
	 * @param array $tags Leave blank, used for recursion
	 * @param boolean $parent_block If parent tag is block element
	 */
	public function parse($tags=array()) {
		global $section, $action, $language, $template_path, $system_template_path;

		if ((!$this->active) && empty($tags))
			return;

		// get language handler for later
		$language_handler = MainLanguageHandler::getInstance();

		// take the tag list for parsing
		$tag_array = (empty($tags)) ? $this->engine->document->tagChildren : $tags;

		// start parsing tags
		$count = count($tag_array);
		for ($i=0; $i<$count; $i++) {
			$tag = $tag_array[$i];

			// if tag has eval set
			if (isset($tag->tagAttrs['cms:eval']) || isset($tag->tagAttrs['eval'])) {
				// get evaluation values
				if (isset($tag->tagAttrs['eval']))
					$value = $tag->tagAttrs['eval']; else
					$value = $tag->tagAttrs['cms:eval'];

				$eval_params = explode(',', $value);

				foreach ($eval_params as $param) {
					// prepare module includes for evaluation
					$settings = array();
					if (!is_null($this->module))
						$settings = $this->module->settings;

					$params = $this->params;
					$to_eval = $tag->tagAttrs[$param];

					$tag->tagAttrs[$param] = eval('global $section, $action, $language, $language_rtl, $language_handler; return '.$to_eval.';');
				}

				// unset param
				unset($tag->tagAttrs['cms:eval']);
			}

			if (isset($tag->tagAttrs['cms:optional'])) {
				// get evaluation values
				$optional_params = explode(',', $tag->tagAttrs['cms:optional']);

				foreach ($optional_params as $param) {
					// prepare module includes for evaluation
					$settings = array();
					if (!is_null($this->module))
						$settings = $this->module->settings;

					$params = $this->params;
					$to_eval = $tag->tagAttrs[$param];

					$value = eval('global $section, $action, $language, $language_rtl, $language_handler; return '.$to_eval.';');

					if ($value == false)
						unset($tag->tagAttrs[$param]); else
						$tag->tagAttrs[$param] = $value;
				}

				// unset param
				unset($tag->tagAttrs['cms:optional']);
			}

			// implement tooltip
			if (isset($tag->tagAttrs['cms:tooltip'])) {
				if (!is_null($this->module))
					$value = $this->module->getLanguageConstant($tag->tagAttrs['cms:tooltip']); else
					$value = $language_handler->getText($tag->tagAttrs['cms:tooltip']);

				$tag->tagAttrs['data-tooltip'] = $value;
				unset($tag->tagAttrs['cms:tooltip']);
			}

			// implement constants
			if (isset($tag->tagAttrs['cms:constant'])) {
				$params = explode(',', $tag->tagAttrs['cms:constant']);

				if (count($params) > 0)
					foreach ($params as $param)
						if (!is_null($this->module))
							$tag->tagAttrs[$param] = $this->module->getLanguageConstant($tag->tagAttrs[$param]); else
							$tag->tagAttrs[$param] = $language_handler->getText($tag->tagAttrs[$param]);

				unset($tag->tagAttrs['cms:constant']);
			}

			// check if specified tag shouldn't be cached
			$skip_cache = false;

			if (isset($tag->tagAttrs['skip_cache'])) {
				// unset param
				unset($tag->tagAttrs['skip_cache']);

				// get cache handler
				$cache = CacheHandler::getInstance();

				// only if current URL is being cached, we start dirty area
				if ($cache->isCaching()) {
					$cache->startDirtyArea();
					$skip_cache = true;

					// reconstruct template for cache,
					// ugly but we are not doing it a lot
					$data = $this->getDataForCache($tag);
					$cache->setCacheForDirtyArea($data);
				}
			}

			// now parse the tag
			switch ($tag->tagName) {
				// handle tag used for setting session variable
				case '_session':
				case 'cms:session':
					$name = $tag->tagAttrs['name'];

					// allow setting referral only once per seesion
					if (isset($tag->tagAttrs['once']))
						$only_once = in_array($tag->tagAttrs['once'], array(1, 'yes')); else
						$only_once = false;

					$should_set = ($only_once && !isset($_SESSION[$name])) || !$only_once;
					
					// store value
					if (!in_array($name, $this->protected_variables) && $should_set) 
						$_SESSION[$name] = $tag->tagAttrs['value'];

					break;
				
				// transfer control to module
				case '_module':
				case 'cms:module':
					if (class_exists($tag->tagAttrs['name'])) {
						$module = call_user_func(array($tag->tagAttrs['name'], 'getInstance'));
						$module->transferControl($tag->tagAttrs, $tag->tagChildren);
					}
					break;

				// load other template
				case '_template':
				case 'cms:template':
					$file = $tag->tagAttrs['file'];
					$path = (key_exists('path', $tag->tagAttrs)) ? $tag->tagAttrs['path'] : '';

					if (!is_null($this->module)) {
						$path = preg_replace('/^%module%/i', $this->module->path, $path);
						$path = preg_replace('/^%templates%/i', $template_path, $path);
					}

					$new = new TemplateHandler($file, $path);
					$new->setLocalParams($this->params);
					$new->parse();
					break;

				// raw text copy
				case '_raw':
				case 'cms:raw':
					if (key_exists('file', $tag->tagAttrs)) {
						// if file attribute is specified
						$file = $tag->tagAttrs['file'];
						$path = (key_exists('path', $tag->tagAttrs)) ? $tag->tagAttrs['path'] : $template_path;

						$text= file_get_contents($path.$file);

					} elseif (key_exists('text', $tag->tagAttrs)) {
						// if text attribute is specified
						$text = $tag->tagAttrs['text'];

					} else {
						// in any other case we display data inside tag
						$text = $tag->tagData;
					}

					echo $text;
					break;

				// multi language constants
				case '_text':
				case 'cms:text':
					$constant = $tag->tagAttrs['constant'];
					$language = (key_exists('language', $tag->tagAttrs)) ? $tag->tagAttrs['language'] : $language;
					$text = "";

					// check if constant is module based
					if (key_exists('module', $tag->tagAttrs)) {
						if (class_exists($tag->tagAttrs['module'])) {
							$module = call_user_func(array($tag->tagAttrs['module'], 'getInstance'));
							$text = $module->getLanguageConstant($constant, $language);
						}
					} else {
						// use default language handler
						$text = MainLanguageHandler::getInstance()->getText($constant, $language);
					}

					echo $text;
					break;

				// support for markdown
				case 'cms:markdown':
					$char_count = isset($tag->tagAttrs['chars']) ? fix_id($tag->tagAttrs['chars']) : null;
					$end_with = isset($tag->tagAttrs['end_with']) ? fix_id($tag->tagAttrs['end_with']) : null;
					$name = isset($tag->tagAttrs['param']) ? $tag->tagAttrs['param'] : null;
					$multilanguage = isset($tag->tagAttrs['multilanguage']) ? $tag->tagAttrs['multilanguage'] == 'yes' : false;

					// get content for parsing
					if (is_null($name))
						$content = $tag->tagData;
						$content = $multilanguage ? $this->params[$name][$language] : $this->params[$name];

					// convert to HTML
					$content = Markdown($content);

					// limit words if specified
					if (!is_null($char_count)) {
						if (is_null($end_with))
							$content = limit_words($content, $char_count); else
							$content = limit_words($content, $char_count, $end_with);
					}

					echo $content;
					break;

				// call section specific data
				case '_section_data':
				case 'cms:section_data':
					if (!is_null($this->module)) {
						$file = $this->module->getSectionFile($section, $action, $language);

						$new = new TemplateHandler(basename($file), dirname($file).'/');
						$new->setLocalParams($this->params);
						$new->setMappedModule($this->module);
						$new->parse();
					} else {
						// log error
						trigger_error('Mapped module is not loaded! File: '.$this->file, E_USER_WARNING);
					}
					break;

				// print multilanguage data
				case '_language_data':
				case 'cms:language_data':
					$name = isset($tag->tagAttrs['param']) ? $tag->tagAttrs['param'] : null;

					if (!isset($this->params[$name]) || !is_array($this->params[$name]) || is_null($name)) break;

					$template = new TemplateHandler('language_data.xml', $system_template_path);
					$template->setMappedModule($this->module);

					foreach($this->params[$name] as $lang => $data) {
						$params = array(
									'param'		=> $name,
									'language'	=> $lang,
									'data'		=> $data,
								);
						$template->restoreXML();
						$template->setLocalParams($params);
						$template->parse();
					}

					break;

				// replace tag data string with matching params
				case '_replace':
				case 'cms:replace':
					$pool = isset($tag->tagAttrs['param']) ? $this->params[$tag->tagAttrs['param']] : $this->params;

					$keys = array_keys($pool);
					$values = array_values($pool);

					foreach($keys as $i => $key)
						$keys[$i] = "%{$key}%";

					// we can't replact string with array, only matching data types
					foreach($values as $i => $value) 
						if (is_array($value)) {
							unset($keys[$i]);
							unset($values[$i]);
						}

					echo str_replace($keys, $values, $tag->tagData);
					break;

				// conditional tag
				case '_if':
				case 'cms:if':
					$settings = !is_null($this->module) ? $this->module->settings : array();
					$params = $this->params;
					$condition = true;

					// check if section is specified and matches
					if (isset($tag->tagAttrs['section']))
						$condition &= $tag->tagAttrs['section'] == $section;

					// check if action is specified and matches
					if (isset($tag->tagAttrs['action']))
						$condition &= $tag->tagAttrs['action'] == $action;

					// check custom condition
					if (isset($tag->tagAttrs['condition'])) {
						$to_eval = $tag->tagAttrs['condition'];
						$eval_result = eval('global $section, $action, $language, $language_rtl, $language_handler; return '.$to_eval.';') == true;
						$condition &= $eval_result;
					}

					// parse children
					if ($condition)
						$this->parse($tag->tagChildren);

					break;

				// conditional tag parsed for desktop version
				case 'cms:desktop':
					if (_DESKTOP_VERSION)
						$this->parse($tag->tagChildren);

					break;

				// conditional tag parsed for mobile version
				case 'cms:mobile':
					if (_MOBILE_VERSION)
						$this->parse($tag->tagChildren);

					break;

				// conditional tag parsed for users that are logged in
				case 'cms:user':
					if ($_SESSION['logged'])
						$this->parse($tag->tagChildren);

					break;

				// conditional tag parsed for guests
				case 'cms:guest':
					if (!$_SESSION['logged'])
						$this->parse($tag->tagChildren);

					break;

				// variable
				case '_var':
				case 'cms:var':
					$settings = array();
					if (!is_null($this->module))
						$settings = $this->module->settings;

					$params = $this->params;
					$to_eval = $tag->tagAttrs['name'];
					echo eval('global $section, $action, $language, $language_rtl, $language_handler; return '.$to_eval.';');
					break;

				// support for script tag
				case 'cms:script':
					if (class_exists('head_tag')) {
						$head_tag = head_tag::getInstance();
						$head_tag->addTag('script', $tag->tagAttrs);
					}
					break;

				// support for collection module
				case 'cms:collection':
					if (array_key_exists('include', $tag->tagAttrs) && class_exists('collection')) {
						$scripts = fix_chars(explode(',', $tag->tagAttrs['include']));

						$collection = collection::getInstance();
						$collection->includeScript($scripts);
					}
					break;

				// support for link tag
				case 'cms:link':
					if (class_exists('head_tag')) {
						$head_tag = head_tag::getInstance();
						$head_tag->addTag('link', $tag->tagAttrs);
					}
					break;

				// support for parameter based choice
				case 'cms:choice':
					$param_value = null;

					if (array_key_exists('param', $tag->tagAttrs)) {
						// grap param value from GET or POST parameters
						$param_name = fix_chars($tag->tagAttrs['param']);
						$param_value = isset($_REQUEST[$param_name]) ? fix_chars($_REQUEST[$param_name]) : null;

					} else if (array_key_exists('value', $tag->tagAttrs)) {
						// use param value specified
						$param_value = fix_chars($tag->tagAttrs['value']);
					}

					// parse only option
					foreach ($tag->tagChildren as $option) {
						if (!$option->tagName == 'option')
							continue;

						$option_value = isset($option->tagAttrs['value']) ? $option->tagAttrs['value'] : null;
						$option_default = isset($option->tagAttrs['default']) ? $option->tagAttrs['default'] == 1 : false;

						// values match or option is default, parse its content
						if ($option_value == $param_value || $option_default) {
							$this->parse($option->tagChildren);
							break;
						}
					}

					break;

				// default action for parser, draw tag
				default:
					if (in_array($tag->tagName, array_keys($this->handlers))) {
						// custom tag handler is set...
						$handle = $this->handlers[$tag->tagName];
						$obj = $handle['object'];
						$function = $handle['function'];

						$obj->$function($tag->tagAttrs, $tag->tagChildren);

					} else {
						// default tag handler
						echo '<'.$tag->tagName.$this->getTagParams($tag->tagAttrs).'>';

						if (count($tag->tagChildren) > 0)
							$this->parse($tag->tagChildren);

						if (count($tag->tagData) > 0)
							echo $tag->tagData;

						$close_tag = $this->close_all_tags ? true : !in_array($tag->tagName, $this->tags_without_end);
						
						if ($close_tag)
							echo '</'.$tag->tagName.'>';
					}
					break;
			}

			// end cache dirty area if initialized
			if ($skip_cache)
				$cache->endDirtyArea();
		}
	}

	/**
	 * Return formated parameter tags
	 *
	 * @param resource $params
	 */
	private function getTagParams($params) {
		$result = "";

		if (count($params))
			foreach ($params as $param=>$value)
				if ($param !== 'eval')
					$result .= ' '.$param.'="'.$value.'"';

		return $result;
	}

	/**
	 * Reconstruct template for cache
	 * 
	 * @param object $tag
	 * @return string
	 */
	private function getDataForCache($tag) {
		$close_tag = $this->close_all_tags ? true : !in_array($tag->tagName, $this->tags_without_end);
		$result = '<'.$tag->tagName.$this->getTagParams($tag->tagAttrs).'>';

		if (count($tag->tagChildren) > 0)
			foreach($tag->tagChildren as $child)
				$result .= $this->getDataForCache($child);

		if (count($tag->tagData) > 0)
			$result .= $tag->tagData;

		if ($close_tag)
			$result .= '</'.$tag->tagName.'>';

		return $result;
	}

	/**
	 * Registers handler function for specified tag
	 *
	 * @param string $tag_name Name of a tag to be handled
	 * @param pointer $object Object with specified public method
	 * @param string $function_name Public tag handler method
	 * @example function tagHandler($level, $params, $children)
	 */
	public function registerTagHandler($tag_name, $object, $function_name) {
		$this->handlers[$tag_name] = array(
					'object' 	=> $object,
					'function'	=> $function_name
				);
	}
}

?>
